# Bibliotecas que vamos a usar
import numpy as np
import matplotlib.pyplot as plt
import cv2
img_path = 'BreCaHAD\BreCaHAD\images\Case_1-01.tif'
groudtruth_path = 'BreCaHAD\BreCaHAD\groundTruth\Case_1-01.json'
# Leer imágenes
img1 = cv2.imread(img_path)
# Cambiar de BGR a RGB
img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)
# Mostrar la imagen
plt.imshow(img1)
<matplotlib.image.AxesImage at 0x15a5f3a1150>
#Transformar a escala de grises
img1_gray = cv2.cvtColor(img1, cv2.COLOR_RGB2GRAY)
plt.imshow(img1_gray, cmap='gray')
<matplotlib.image.AxesImage at 0x15a5f5deec0>
blue_channel = img1[:, :, 0] # 0 corresponde al canal azul
# Crear una imagen en negro con el mismo tamaño que la original
blue_img = np.zeros_like(img1)
#Asignar el canal azul a la imagen
blue_img[:, :, 0] = blue_channel
# establecer los canales verde y rojo en 0 (negro)
blue_img[:, :, 1] = 0
blue_img[:, :, 2] = 0
blue_img = cv2.cvtColor(blue_img, cv2.COLOR_BGR2GRAY)
plt.imshow(blue_img, cmap = 'gray')
<matplotlib.image.AxesImage at 0x15a5f64f460>
# Mínimo y máximo en imagen en escala de grises
# blue_img.min()
blue_img.max()
29
img_nuclei = blue_img.copy()
img_nuclei[img_nuclei < blue_img.max()/2 + 1] = 0 # menos a 15 en negro
img_nuclei[img_nuclei >= blue_img.max()/2 + 1] = 255
plt.imshow(img_nuclei, cmap = 'gray')
<matplotlib.image.AxesImage at 0x15a6138eaa0>
# Para ver a color las partes seleccionadas
img_selected = img1.copy()
img_selected[(img_nuclei == 255) == True] = 255
plt.imshow(img_selected) # Básicamente no hay eosina
<matplotlib.image.AxesImage at 0x15a61405ff0>
# Necesitamos que los núcleos sean blancos y el fondo negro
nuclei_foreground = img_nuclei.copy()
nuclei_foreground[nuclei_foreground == 255 ] = 100
nuclei_foreground[nuclei_foreground != 100 ] = 255
nuclei_foreground[nuclei_foreground == 100 ] = 0
# Este es el "sure foreground" que se usa en la transformación de Watershed
plt.imshow(nuclei_foreground, cmap = "gray")
<matplotlib.image.AxesImage at 0x15a5f465b10>
# Hay muchos espacios a los interno del núcleo, vamos a tratar de llenarlo con operaciones morfológicas
# Vamos a usar la operación CLOSING para llenar espacios
kernel = np.ones((3,3),np.uint8)
### BORRARRRRR # Primero eliminar ruido
opening = cv2.morphologyEx(nuclei_foreground, cv2.MORPH_OPEN, kernel, iterations = 0)
kernel = np.ones((6,6),np.uint8)
nuclei_foreground_closing = cv2.morphologyEx(opening, cv2.MORPH_CLOSE, kernel, iterations = 1)
plt.imshow(opening, cmap = "gray")
<matplotlib.image.AxesImage at 0x15a5f4d54e0>
# Para estar más seguros de lo que es "núcleo"
nuclei_foreground_erosion = cv2.erode(nuclei_foreground_closing, kernel, iterations = 0)
plt.imshow(nuclei_foreground_erosion, cmap = "gray")
<matplotlib.image.AxesImage at 0x15a5f540b50>
# Vamos a a rellanar "más" los espacios para evitar que haya "background" en los nucleos
kernel = np.ones((3,3),np.uint8)
#background = cv2.morphologyEx(nuclei_foreground_closing, cv2.MORPH_CLOSE, kernel, iterations = 5)
background = cv2.dilate(nuclei_foreground_closing,kernel,iterations = 1)
plt.imshow(background, cmap = "gray")
<matplotlib.image.AxesImage at 0x15a62c48280>
prueba = img1.copy()
prueba[(background == 0) == True] = 255
plt.imshow(prueba)
<matplotlib.image.AxesImage at 0x15a62c83a00>
unknow_area = cv2.subtract(background, nuclei_foreground_erosion)
plt.imshow(unknow_area, cmap = "gray")
<matplotlib.image.AxesImage at 0x15a62ceb520>
# Obtener marcadores
ret, markers = cv2.connectedComponents(nuclei_foreground_erosion)
# Agregamos 1 para que ningún marcador sea 0
markers = markers+1
# Marcamos las regiones desconocidas con 0
markers[unknow_area == 255] = 0
nuclei_segmented = img1.copy()
markers = cv2.watershed(nuclei_segmented, markers)
nuclei_segmented[markers == -1] = [255,0,0]
plt.imshow(nuclei_segmented)
nuclei_segmented = cv2.cvtColor(nuclei_segmented, cv2.COLOR_RGB2BGR)
cv2.imwrite('nucleo_water6x6x1.png', nuclei_segmented)
True
# Obtenemos los núcleos dentro de los bordes
mask_nuclei = np.zeros_like(img1_gray, dtype=np.uint8)
mask_nuclei[markers != 1] = 255
nuclei_after_water = cv2.bitwise_and(img1, img1, mask=mask_nuclei)
plt.imshow(nuclei_after_water)
<matplotlib.image.AxesImage at 0x15a62b1f0d0>
nuclei_white = cv2.cvtColor(nuclei_after_water, cv2.COLOR_RGB2GRAY)
# fondo negro y objetos en blanco
nuclei_white[nuclei_white != 0] = 255
plt.imshow(nuclei_white, cmap = "gray")
<matplotlib.image.AxesImage at 0x15a62b7e830>
# Parece que el algoritmo que esta usando connectedComponentsWithStats() considera que los objetos en bordes opuestos
# son los mismos. Para correguirlo, vamos a eliminar una pequeña sección de los bordes
# Eliminar objetos pequeños en los bordes
border_size = 5 # Tamaño del borde para considerar
nuclei_white[:border_size, :] = 0
nuclei_white[-border_size:, :] = 0
nuclei_white[:, :border_size] = 0
nuclei_white[:, -border_size:] = 0
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(nuclei_white, connectivity = 8)
# Cantidad de componentes conectados
num_labels
470
# Vemos el área de los objetos
stats[:, cv2.CC_STAT_AREA]
array([1034517, 170, 10667, 249, 157, 13, 108,
206, 4, 4040, 175, 13, 1167, 589,
714, 624, 5, 47, 24, 185, 13,
12, 406, 277, 50, 71, 11, 16,
50, 477, 1153, 44, 130, 330, 20,
8, 319, 12, 512, 72, 116, 313,
529, 340, 540, 54, 357, 827, 340,
90, 359, 498, 237, 2400, 219, 8,
375, 430, 115, 533, 282, 2003, 300,
933, 564, 302, 5, 231, 12, 112,
1034, 1080, 3672, 11, 51, 3873, 91,
5, 131, 8, 41, 12, 5, 1601,
283, 4076, 10, 895, 1953, 5800, 5,
267, 5, 1305, 819, 45, 201, 165,
75, 1141, 940, 6809, 157, 14, 240,
37, 103, 2882, 34, 20, 14, 367,
1320, 12, 1178, 13, 5, 5, 2030,
103, 697, 22, 48, 644, 10, 952,
8, 12, 274, 780, 622, 202, 10791,
31, 43, 560, 220, 415, 3931, 984,
294, 607, 330, 61, 36, 8, 316,
40, 1335, 72, 5, 670, 2950, 5,
5034, 3949, 62, 45, 422, 2324, 87,
513, 5, 301, 906, 8, 5, 25,
520, 11, 18, 29, 934, 604, 26,
32, 51, 5, 369, 1083, 23, 5,
16, 23, 21, 186, 330, 1461, 8397,
175, 176, 967, 37, 5, 5, 205,
370, 10, 497, 10, 46, 8, 5,
106, 294, 2406, 5, 52, 895, 321,
244, 99, 12, 1573, 178, 91, 209,
225, 1126, 2194, 14, 11, 245, 784,
42, 2153, 327, 29, 4567, 236, 33,
81, 8, 510, 157, 55, 411, 51,
278, 1546, 456, 12, 1031, 430, 873,
378, 5, 418, 2043, 1241, 2356, 8,
5, 5, 17, 2039, 18, 12, 1285,
5, 21, 1748, 394, 115, 601, 482,
3997, 1556, 5340, 32, 5, 68, 67,
5, 12, 12, 5, 55, 18, 1567,
148, 355, 27, 30, 4927, 145, 8,
24, 952, 51, 3760, 5, 5, 21,
1085, 4127, 37, 8, 660, 5, 462,
424, 547, 64, 18408, 99, 5, 394,
2201, 2145, 33, 3871, 27, 942, 106,
451, 870, 157, 96, 669, 2103, 1130,
1819, 858, 665, 621, 1515, 515, 8661,
535, 20, 8, 8, 1997, 5, 586,
41, 1472, 618, 1143, 786, 14, 2231,
353, 2535, 37, 700, 2188, 765, 5,
142, 1180, 533, 543, 764, 421, 5,
5, 383, 1027, 14, 5, 1422, 14,
982, 12, 213, 986, 10, 8, 7721,
8, 408, 1127, 145, 110, 448, 6242,
148, 16, 1199, 30, 10, 693, 1480,
6873, 219, 17, 36, 27, 2729, 58,
11, 4446, 79, 1175, 438, 408, 1009,
26, 548, 861, 21, 12, 38, 5,
2788, 86, 890, 122, 545, 820, 359,
126, 1208, 504, 5, 1068, 84, 107,
2479, 283, 1837, 1155, 466, 53, 166,
3164, 1618, 94, 186, 939, 232, 193,
8, 32, 322, 849, 8, 190, 1550,
12, 481, 5, 121, 194, 66, 680,
666, 1856, 473, 1248, 576, 144, 1318,
1237, 33, 347, 288, 58, 571, 486,
548, 235, 271, 313, 512, 790, 51,
99], dtype=int32)
# Tambien se pueden extraer otras caracteristicas de esos componentes, como:
# Altura del cuadro delimitador
# stats[:, cv2.CC_STAT_HEIGHT]
# Ancho del cuadro delimitador
#stats[:, cv2.CC_STAT_WIDTH]
# Los centroides están en variable `centroids`
# Definimos un tamaño mínimo
min_size = 800 # ELiminamos los objemos menores a este tamaño
# Generamos un "lienzo" en negro
nuclei_without_smalls = np.zeros((nuclei_white.shape))
for i in range(1, num_labels):
if stats[i, cv2.CC_STAT_AREA] >= min_size:
nuclei_without_smalls[labels == i] = 255
plt.imshow(nuclei_without_smalls, cmap = 'gray')
<matplotlib.image.AxesImage at 0x15a651d5ed0>
nuclei_with_borders = nuclei_without_smalls.copy()
# Ponemos en negro los bordes obtenidos con Watershed
nuclei_with_borders[markers == -1] = 0
plt.imshow(nuclei_with_borders, cmap = 'gray') # Lo que se gana es poco
cv2.imwrite('nuclei_gray.png', nuclei_with_borders)
True
# Núcleos
nuclei_color = img1.copy()
nuclei_color[(nuclei_without_smalls == 0) == True] = 0
plt.imshow(nuclei_color)
nuclei_color = cv2.cvtColor(nuclei_color, cv2.COLOR_RGB2BGR)
cv2.imwrite('nuclei_color.png', nuclei_color)
True
# Fondo
background_color = cv2.subtract(img1, nuclei_color)
plt.imshow(background_color)
<matplotlib.image.AxesImage at 0x15a650bf310>
import json
with open(groudtruth_path, 'r') as file:
# Carga el contenido del archivo JSON
groundTruth_img1 = json.load(file)
# Eliminar la clase lumen y non_lumen que forman parte del background
del groundTruth_img1['lumen']
del groundTruth_img1['non_lumen']
img1.shape
(1024, 1360, 3)
img1_points = img1.copy()
nuclei_points = nuclei_color.copy()
height = img1.shape[0]
width = img1.shape[1]
width
1360
# Asignar un color para cada clase
cell_color = {
'mitosis': (0, 0, 255), # azul
'non_tumor': (0, 255, 0), # Verde
'apoptosis': (255, 0, 0), # rojo
'tumor': (255, 255, 0), # Amarillo
'non_mitosis': (0, 0, 0), # Negro
}
# Dibujar círculos en la imagen basándote en las coordenadas del JSON
img_with_points = np.zeros_like(img1)
# extraer valores
total_nuclei_groundTruth = 0
identified_nuclei = 0
for cell_type, nuclei in groundTruth_img1.items():
color = cell_color.get(cell_type, (255, 255, 255)) # blanco si no está definido
for point in nuclei:
x = int(point['x'] * width)
y = int(point['y'] * height) # Usar img.shape[0] para obtener la altura
cv2.circle(img1_points, (x, y), 10, color, -1) # Dibuja círculos de radio 10 en color rojo (BGR)
cv2.circle(nuclei_points, (x, y), 10, color, -1)
cv2.circle(img_with_points, (x,y), 10, (255,255,255) , -1)
# Cantidad total de núcleos
total_nuclei_groundTruth +=1
# Cantidad de ocaciones donde el pixel del "groudtruth" corresponde a un pixel de núcleo
pixel = nuclei_color[y][x]
if (pixel[0] != 0 and pixel[1] != 0 and pixel[2] != 0):
identified_nuclei +=1
print("La cantidad total de núcleos en el `groundTruth` es", total_nuclei_groundTruth)
print("De eso se segmentaron ", identified_nuclei)
print ("Para un procentaje de", identified_nuclei/total_nuclei_groundTruth*100)
# Este porcentaje es igual a la sencibilidad:
# verdaderos_positivos / (verdaderos_positivos + falsos_negativos)
# identified_nuclei / (identified_nuclei + (total_nuclei_groundTruth - identified_nuclei))
La cantidad total de núcleos en el `groundTruth` es 141 De eso se segmentaron 121 Para un procentaje de 85.81560283687944
nuclei_points.shape
(1024, 1360, 3)
# Mostrar resultado
plt.imshow(img1_points) # los puntos corresponden al groudtruth
<matplotlib.image.AxesImage at 0x15a65136860>
plt.imshow(nuclei_points)
<matplotlib.image.AxesImage at 0x15a6b49abc0>
plt.imshow(img_with_points)
<matplotlib.image.AxesImage at 0x15a6b4fa200>
def groundTruth_inside_square(groundTruth, x, y, width, height, image_width, image_height):
for cell_type, nuclei in groundTruth.items():
for point in nuclei:
pixel_x = int(point['x']*image_width)
pixel_y = int(point['y']*image_height)
if (x <= pixel_x <= x + width) and (y <= pixel_y <= y + height):
return True
break
return False
nuclei_without_smalls_8u = nuclei_without_smalls.astype(np.uint8) #necesita que sean enteros sin signo
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(nuclei_without_smalls_8u, connectivity = 8)
n_nuclei_segmented = 0
not_in_groud_truth = 0
squere_components = np.zeros_like(img1)
# Dibujar el cuadro delimitador de cada componente conectado
for i in range(1, num_labels): # Empezar desde 1 para omitir el fondo (etiqueta 0)
x, y, w, h, area = stats[i]
n_nuclei_segmented += 1
if (not groundTruth_inside_square(groundTruth_img1, x, y, w, h, width, height)):
not_in_groud_truth += 1
# Dibujar un cuadrado delimitador en el lienzo
cv2.rectangle(squere_components, (x, y), (x+w, y+h), (255, 255, 255), -1)
plt.imshow(squere_components)
print("La cantidad de objetos segmentados fue", n_nuclei_segmented)
print("De esos", not_in_groud_truth , "no estaban en el `groudtruth` ")
La cantidad de objetos segmentados fue 117 De esos 36 no estaban en el `groudtruth`
plt.imshow(img_with_points)
<matplotlib.image.AxesImage at 0x15a6b394e20>
# verdaderos_positivos / (verdaderos_positivos + falsos_positivos)
identified_nuclei / (identified_nuclei + not_in_groud_truth)
0.7707006369426752
identified_nuclei / (identified_nuclei + (total_nuclei_groundTruth - identified_nuclei))
0.8581560283687943
# Métricas para imagenes de prueba fueron
# Case_1-01.tif | Precisión: 0.7555555555555555 | sencibilidad: 0.9645390070921985
# Case_1-03.tif | Precisión: 0.7978723404255319 | sencibilidad: 0.8771929824561403
# Case_1-03.tif | Precisión: 0.770949720670391 | sencibilidad: 0.9019607843137255
# Case_1-04.tif | Precisión: 0.7536231884057971 | sencibilidad:0.9043478260869565
# Case_1-05.tif | Precisión: 0.7441860465116279 | sencibilidad: 0.9552238805970149
# Case_1-06.tif | Precisión: 0.7479338842975206 | sencibilidad: 0.8916256157635468
# Case_1-07.tif | Precisión: 0.871244635193133 | sencibilidad: 0.8982300884955752
# Case_1-08.tif | Precisión: 0.76 | sencibilidad: 0.957983193277311